﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Media;
using System.Windows.Documents;
using System.ComponentModel;
using SDF.Primitives;
using System.Collections.ObjectModel;
using System.Collections.Specialized;

namespace SDF
{
    public abstract class SmartTextElement : UIElement, ISmartDataObject
    {
        #region Constructors

        protected SmartTextElement()
        {
        }

        #endregion

        #region Dependency Properties

        public static readonly DependencyProperty BackgroundProperty = TextElement.BackgroundProperty.AddOwner(typeof(SmartTextElement), new FrameworkPropertyMetadata(
            TextElement.BackgroundProperty.DefaultMetadata.DefaultValue, FrameworkPropertyMetadataOptions.AffectsRender, OnRenderAffected));
        public static readonly DependencyProperty FlowDirectionProperty = Block.FlowDirectionProperty.AddOwner(typeof(SmartTextElement), new FrameworkPropertyMetadata(
            Block.FlowDirectionProperty.DefaultMetadata.DefaultValue, FrameworkPropertyMetadataOptions.AffectsRender, OnRenderAffected));
        public static readonly DependencyProperty ForegroundProperty = TextElement.ForegroundProperty.AddOwner(typeof(SmartTextElement), new FrameworkPropertyMetadata(
            TextElement.ForegroundProperty.DefaultMetadata.DefaultValue, FrameworkPropertyMetadataOptions.AffectsRender, OnRenderAffected));
        public static readonly DependencyProperty FontFamilyProperty = TextElement.FontFamilyProperty.AddOwner(typeof(SmartTextElement), new FrameworkPropertyMetadata(
            TextElement.FontFamilyProperty.DefaultMetadata.DefaultValue, FrameworkPropertyMetadataOptions.AffectsMeasure, OnMeasureAffected));
        public static readonly DependencyProperty FontSizeProperty = TextElement.FontSizeProperty.AddOwner(typeof(SmartTextElement), new FrameworkPropertyMetadata(
            TextElement.FontSizeProperty.DefaultMetadata.DefaultValue, FrameworkPropertyMetadataOptions.AffectsMeasure, OnMeasureAffected));
        public static readonly DependencyProperty FontStretchProperty = TextElement.FontStretchProperty.AddOwner(typeof(SmartTextElement), new FrameworkPropertyMetadata(
            TextElement.FontStretchProperty.DefaultMetadata.DefaultValue, FrameworkPropertyMetadataOptions.AffectsMeasure, OnMeasureAffected));
        public static readonly DependencyProperty FontStyleProperty = TextElement.FontStyleProperty.AddOwner(typeof(SmartTextElement), new FrameworkPropertyMetadata(
            TextElement.FontStyleProperty.DefaultMetadata.DefaultValue, FrameworkPropertyMetadataOptions.AffectsMeasure, OnMeasureAffected));
        public static readonly DependencyProperty FontWeightProperty = TextElement.FontWeightProperty.AddOwner(typeof(SmartTextElement), new FrameworkPropertyMetadata(
            TextElement.FontWeightProperty.DefaultMetadata.DefaultValue, FrameworkPropertyMetadataOptions.AffectsMeasure, OnMeasureAffected));
        public static readonly DependencyProperty TextAlignmentProperty = Block.TextAlignmentProperty.AddOwner(typeof(SmartTextElement), new FrameworkPropertyMetadata(
            Block.TextAlignmentProperty.DefaultMetadata.DefaultValue, FrameworkPropertyMetadataOptions.AffectsMeasure, OnMeasureAffected));
        public static readonly DependencyProperty TextEffectsProperty = Block.TextEffectsProperty.AddOwner(typeof(SmartTextElement), new FrameworkPropertyMetadata(
            Block.TextEffectsProperty.DefaultMetadata.DefaultValue, FrameworkPropertyMetadataOptions.AffectsMeasure, OnMeasureAffected));


        #region "Actual" Properties

        private static readonly DependencyPropertyKey ActualHeightPropertyKey = DependencyProperty.RegisterReadOnly(
            "ActualHeight",
            typeof(double),
            typeof(SmartTextElement),
            new FrameworkPropertyMetadata(0.0));

        public static readonly DependencyProperty ActualHeightProperty = ActualHeightPropertyKey.DependencyProperty;

        private static readonly DependencyPropertyKey ActualWidthPropertyKey = DependencyProperty.RegisterReadOnly(
            "ActualWidth",
            typeof(double),
            typeof(SmartTextElement),
            new FrameworkPropertyMetadata(0.0));

        public static readonly DependencyProperty ActualWidthProperty = ActualWidthPropertyKey.DependencyProperty;

        #endregion

        #endregion

        #region Properties

        public Brush Background
        {
            get { return (Brush)GetValue(BackgroundProperty); }
            set { SetValue(BackgroundProperty, value); }
        }

        public FlowDirection FlowDirection
        {
            get { return (FlowDirection)GetValue(FlowDirectionProperty); }
            set { SetValue(FlowDirectionProperty, value); }
        }

        public Brush Foreground
        {
            get { return (Brush)GetValue(ForegroundProperty); }
            set { SetValue(ForegroundProperty, value); }
        }

        public FontFamily FontFamily
        {
            get { return (FontFamily)GetValue(FontFamilyProperty); }
            set { SetValue(FontFamilyProperty, value); }
        }

        public double FontSize
        {
            get { return (double)GetValue(FontSizeProperty); }
            set { SetValue(FontSizeProperty, value); }
        }

        public FontStretch FontStretch
        {
            get { return (FontStretch)GetValue(FontStretchProperty); }
            set { SetValue(FontStretchProperty, value); }
        }

        public FontStyle FontStyle
        {
            get { return (FontStyle)GetValue(FontStyleProperty); }
            set { SetValue(FontStyleProperty, value); }
        }

        public FontWeight FontWeight
        {
            get { return (FontWeight)GetValue(FontWeightProperty); }
            set { SetValue(FontWeightProperty, value); }
        }

        public double LineHeight
        {
            get { return FontFamily.LineSpacing * FontSize; }
        }

        public TextAlignment TextAlignment
        {
            get { return (TextAlignment)GetValue(TextAlignmentProperty); }
            set { SetValue(TextAlignmentProperty, value); }
        }
        
        public TextEffectCollection TextEffects
        {
            get { return (TextEffectCollection)GetValue(TextEffectsProperty); }
            set { SetValue(TextEffectsProperty, value); }
        }

        protected virtual bool ShouldRenderBackground
        {
            get { return true; }
        }

        #endregion

        #region Background Rendering

        protected override void OnRender(DrawingContext drawingContext)
        {
            if (ShouldRenderBackground)
            {
                Brush background = this.Background;
                if (background != null)
                {
                    drawingContext.DrawRectangle(background, null, new Rect(0.0, 0.0, base.RenderSize.Width, base.RenderSize.Height));
                }
            }
        }

        #endregion

        #region CopyProperties

        protected virtual void CopyProperties(SmartTextElement copy)
        {
            copy.Background = this.Background;
            copy.FlowDirection = this.FlowDirection;
            copy.FontFamily = this.FontFamily;
            copy.FontSize = this.FontSize;
            copy.FontStretch = this.FontStretch;
            copy.FontStyle = this.FontStyle;
            copy.FontWeight = this.FontWeight;
            copy.Foreground = this.Foreground;
            copy.TextAlignment = this.TextAlignment;
            copy.TextEffects = this.TextEffects;
        }

        #endregion

        #region OnMeasureAffected and OnRenderAffected Methods

        protected virtual void InvalidateRender()
        {
            this.InvalidateMeasure();
            this.InvalidateArrange();
            this.InvalidateVisual();
        }

        protected static void OnMeasureAffected(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var element = sender as SmartTextElement;
            if (element != null)
                element.InvalidateRender();
        }

        protected static void OnRenderAffected(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var element = sender as SmartTextElement;
            if (element != null)
                element.InvalidateVisual();
        }

        #endregion

        #region Overriden methods

        protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
        {
            base.OnPropertyChanged(e);
            OnPropertyChanged(e.Property.Name);
        }

        #endregion

        #region AttachedObjects and AttachedCollections properties

        /// <summary>
        /// The collection of child objects that this object is composed of.
        /// </summary>
        /// <remarks>
        /// This represents a composition relationship, where the attached object is part of the containing object.
        /// </remarks>
        protected Collection<ISmartDataObject> AttachedObjects
        {
            get
            {
                if (m_AttachedObjects == null)
                    m_AttachedObjects = new Collection<ISmartDataObject>();
                return m_AttachedObjects;
            }
        }
        private Collection<ISmartDataObject> m_AttachedObjects;


        /// <summary>
        /// The list of attached non-composition collections.
        /// </summary>
        /// <remarks>
        /// Adding or removing objects to these collections causes changes to the data of the containing object to change state,
        /// but when an item in the collecion changes its data the containing object does not. This represents a one-to-many or
        /// many-to-many association relationship.
        /// </remarks>
        protected Collection<INotifyCollectionChanged> AttachedCollections
        {
            get
            {
                if (m_AttachedCollections == null)
                    m_AttachedCollections = new Collection<INotifyCollectionChanged>();
                return m_AttachedCollections;
            }
        }
        private Collection<INotifyCollectionChanged> m_AttachedCollections;

        /// <summary>
        /// The list of attached composition collections.
        /// </summary>
        /// <remarks>
        /// Adding or removing objects to these collections causes changes to the data of the containing object to change state,
        /// and when an item in the collecion changes its data the containing object does too. This represents a one-to-many 
        /// composition relationship (i.e. order and order items).
        /// </remarks>
        protected Collection<ISmartDataObjectCollection> ComposedCollections
        {
            get
            {
                if (m_ComposedCollections == null)
                    m_ComposedCollections = new Collection<ISmartDataObjectCollection>();
                return m_ComposedCollections;
            }
        }
        private Collection<ISmartDataObjectCollection> m_ComposedCollections;

        #endregion

        #region Attachment methods

        /// <summary>
        /// Attachs to another object, so that this object will become dirty if the other object changes.
        /// </summary>
        /// <param name="obj">The object to attach to.</param>
        protected void Attach(ISmartDataObject obj)
        {
            if (obj == null)
                return;

            if (!AttachedObjects.Contains(obj))
                AttachedObjects.Add(obj);

            obj.DataChanged += new EventHandler<DataChangedEventArgs>(OnChildDataChanged);
        }

        /// <summary>
        /// Detaches from another object, so that this object will not become dirty if the other object changes.
        /// </summary>
        /// <param name="obj">The object to detach from.</param>
        protected void Detach(ISmartDataObject obj)
        {
            if (obj == null)
                return;

            if (AttachedObjects.Contains(obj))
                AttachedObjects.Remove(obj);

            obj.DataChanged -= new EventHandler<DataChangedEventArgs>(OnChildDataChanged);
        }

        protected virtual void OnChildDataChanged(object sender, DataChangedEventArgs e)
        {
            this.OnDataChanged(e);
        }

        /// <summary>
        /// Attachs to an INotifyCollectionChanged, so that this object will become dirty if the collection changes.
        /// </summary>
        /// <param name="collection">The collection to attach to.</param>
        protected void Attach(INotifyCollectionChanged collection)
        {
            if (collection == null)
                throw new ArgumentNullException("collection");

            ISmartDataObjectCollection dataObjectCollection = collection as ISmartDataObjectCollection;
            if (dataObjectCollection != null)
            {
                AttachCollection(dataObjectCollection, true);
            }
            else
            {
                if (!AttachedCollections.Contains(collection))
                    AttachedCollections.Add(collection);
                collection.CollectionChanged += new NotifyCollectionChangedEventHandler(OnCollectionChanged);
            }
        }


        /// <summary>
        /// Attachs to an IDataObjectCollection, so that this object will become dirty if the collection changes.
        /// </summary>
        /// <param name="collection">The collection to attach to.</param>
        /// <param name="isComposition">When true, makes this object dirty if the objects on the collection become dirty as well.</param>
        protected void AttachCollection(ISmartDataObjectCollection collection, bool isComposition)
        {
            if (isComposition)
            {
                if (!ComposedCollections.Contains(collection))
                    ComposedCollections.Add(collection);
                collection.DataChanged += new EventHandler<DataChangedEventArgs>(OnCollectionDataChanged);
            }
            else
            {
                if (!AttachedCollections.Contains(collection))
                    AttachedCollections.Add(collection);
                collection.CollectionChanged += new NotifyCollectionChangedEventHandler(OnCollectionChanged);
            }
        }

        /// <summary>
        /// Detaches from an IDataObjectCollection, so that this object will not become dirty if the collection changes.
        /// </summary>
        /// <param name="collection">The collection to attach to.</param>
        protected void DetachCollection(INotifyCollectionChanged collection)
        {
            if (AttachedCollections.Contains(collection))
                AttachedCollections.Remove(collection);
            collection.CollectionChanged -= new NotifyCollectionChangedEventHandler(OnCollectionChanged);

            ISmartDataObjectCollection dataObjectCollection = collection as ISmartDataObjectCollection;
            if (dataObjectCollection != null)
            {
                if (ComposedCollections.Contains(dataObjectCollection))
                    ComposedCollections.Remove(dataObjectCollection);
                dataObjectCollection.DataChanged -= new EventHandler<DataChangedEventArgs>(OnCollectionDataChanged);
            }
        }

        protected virtual void OnCollectionDataChanged(object sender, DataChangedEventArgs e)
        {
            this.OnDataChanged(e);
        }

        protected virtual void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            this.OnDataChanged(new DataChangedEventArgs(sender, e, IsVisualCollection(sender)));
        }

        protected virtual bool IsVisualCollection(object collection)
        {
            return false;
        }

        #endregion

        #region INotifyPropertyChanged Event

        /// <summary>
        /// Raised when a property of the object changes.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged
        {
            add { m_PropertyChanged += value; }
            remove { m_PropertyChanged -= value; }
        }

        private PropertyChangedEventHandler m_PropertyChanged;

        /// <summary>
        /// Raises the PropertyChanged event for the given property and any properties dependent upon it.
        /// </summary>
        /// <param name="propertyName">The name of the property that changed.</param>
        protected virtual void OnPropertyChanged(string propertyName)
        {
            if (m_PropertyChanged != null)
            {
                // Notify that the property changed
                m_PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

                // Notify that data has changed
                PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
                OnDataChanged(new DataChangedEventArgs(this, e, IsVisualProperty(propertyName)));

                // Notify changes to dependant properties
                foreach (string property in DependsUponAttribute.GetDependantPropertiesRecursive(propertyName, this.GetType()))
                    m_PropertyChanged(this, new PropertyChangedEventArgs(property));
            }

        }

        protected virtual bool IsVisualProperty(string propertyName)
        {
            return false;
        }

        #endregion

        #region INotifyDataChanged Event

        /// <summary>
        /// raised when data about the object changes (making the object "dirty").
        /// </summary>
        /// <remarks>
        /// <p>The DataChanged event differs slightly from the property changed event in the following ways:</p>
        /// <ul>
        ///   <li>It is only fired for data properties. For example, it is not fired when IsDirty changes.</li>
        ///   <li>It is also fired when data from attached objects or collections change (e.g. the property that change does not belong to 
        ///       the object itself - it belongs to an attached (child) object).</li>
        /// </ul>
        /// </remarks>
        public event EventHandler<DataChangedEventArgs> DataChanged
        {
            add { m_DataChanged += value; }
            remove { m_DataChanged -= value; }
        }
        private EventHandler<DataChangedEventArgs> m_DataChanged;

        /// <summary>
        /// Raises the DataChanged event.
        /// </summary>
        /// <param name="e">The argument of the event that caused the data changed.</param>
        protected virtual void OnDataChanged(DataChangedEventArgs e)
        {
            if (m_DataChanged != null)
                m_DataChanged(this, e);
        }

        #endregion
    }
}
